(in-package :experiment)

(define-condition audio-error (simple-condition)
  ())

(defstruct recorder-stream
  input output error)

(defclass audio-record (subsequence)
  ((audio-stream :initform nil :accessor audio-stream)
   (volume-process :initform nil :accessor volume-process)
   (volume-lock :initform (gwee::make-lock) :accessor volume-lock)
   (current-volume :initform 0)
   (volume-hook :initform nil :accessor volume-hook)))

(defmethod current-volume ((obj audio-record))
  ;; I don't think slot accesses are atomic are they?
  (with-slots (current-volume volume-lock) obj
    (gwee::with-lock (volume-lock)
      current-volume)))

(defun monitor-volume (state)
  "A monitor function run in its own thread that updates the
current-volume slot."
  (with-slots (current-volume audio-stream volume-lock volume-hook) state
    ;;(print `(checking for data))
    (when (listen (recorder-stream-input audio-stream))
      ;; we know its always a float so we should be fine, right?
      (let ((volume (read-from-string (read-line (recorder-stream-input audio-stream)))))
        ;; (print `(got it ,volume))
        ;; (finish-output)
        (gwee::with-lock (volume-lock)
          (setf current-volume volume))
        (when volume-hook
          (funcall volume-hook volume))))))

(defmethod state-init (exp (state audio-record))
  (handler-case
        (setf (audio-stream state) (start-audio-recording (experiment-id exp))
              ;; (volume-process state)
              ;; (gwee:process-run-function "volume monitor" (lambda () (monitor-volume state)))
              ;; (gwee:run-with-timer 0 1 (lambda () (monitor-volume state)))
              )
    (audio-error (c)
      (sdl:fill-surface (gwee:gray 0))
      (gwee:display-message (format nil "Failed to initialize audio: ~a" c))
      nil))
  (setf gwee::*tick-hook* (lambda (time) (declare (ignore time)) (monitor-volume state)))
  (lg 'start :index (subsequence-index state)))

(defmethod state-clean-up progn (exp (state audio-record))
  (declare (ignore exp))
  (when (audio-stream state)
    (end-audio-recording (audio-stream state)))
  ;; (when (volume-process state)
  ;;   (gwee:process-kill (volume-process state)))
  )

(defun find-recorder-program ()
  (let ((program (gwee:resource "RecordAudioToFile")))
    (print program)
    (when (probe-file program)
      program)))

(defun current-datetime (&optional (time-delim ":"))
  (multiple-value-bind (s m h d mm y) (get-decoded-time)
    (format nil "~d-~2,'0d-~2,'0d--~2,'0d~a~2,'0d~a~2,'0d" y mm d h time-delim m time-delim s)))

(defun start-audio-recording (id)
  (let ((program (find-recorder-program)))
    (if program
        (progn
          (multiple-value-bind (input output err)
              (gwee:run-program (namestring program)
                                (list (gwee:top-level-path "audio/")
                                      (format nil "audio-~a--~a.mp4" id (current-datetime "-")))
                                :wait nil
                                :output :stream
                                :input :stream
                                :error :stream
                                ;; We have a thread that will monitor
                                ;; volume levels. So to be safe, use a
                                ;; lock when reading/writing.
                                ;; :sharing :external
                                )
            ;; the program will print errors to err and if no error occurs, it
            ;; will print a line to output.
            (loop
              with start = (get-internal-real-time)
              until (or (listen input)
                        (listen err)
                        (> (/ (- start (get-internal-real-time))
                              internal-time-units-per-second) 5))
              do (sdl-cffi::sdl-delay 100))
            ;; we can listen on input first because the only things
            ;; coming out of the recorder's stdout is the begin message
            ;; which is printed from the audio data callback. So if we
            ;; get that message we KNOW we are recording. Anything on
            ;; the err stream is a buffer under-run.
            (cond ((listen err)
                   (signal 'audio-error :format-control "Audio Error: ~a" :format-arguments (list (read-line err))))
                  ((listen input)
                   (let ((line (read-line input)))
                     (unless (string= line "hit a key to stop.")
                       (signal 'audio-error :format-control "Audio Error: ~a" :format-arguments (list line)))))
                  (t
                   (signal 'audio-error :format-control "I waited 5 seconds for the recorder but it never booted!")))
            (make-recorder-stream :input input :output output :error err)))
        (signal 'audio-error :format-control "Unable to find recording program."))))

(defun end-audio-recording (stream)
  (write-line "stop." (recorder-stream-output stream))
  ;; we need to push this out to ensure the audio stops.
  (finish-output (recorder-stream-output stream)))
